package bma.common.netty.proxy;

import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandler;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;

import bma.common.netty.NettyUtil;

public abstract class ProxyHandler extends SimpleChannelUpstreamHandler {

	final org.slf4j.Logger log = org.slf4j.LoggerFactory
			.getLogger(ProxyHandler.class);

	private volatile Channel outboundChannel;

	public ProxyHandler() {
		super();
	}

	protected abstract ClientBootstrap getRemoteBootstrap(
			ChannelHandlerContext ctx, ChannelStateEvent e,
			ChannelHandler handler) throws Exception;

	protected abstract String proxyTitle();

	@Override
	public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e)
			throws Exception {
		// Suspend incoming traffic until connected to the remote host.
		final Channel inboundChannel = e.getChannel();
		inboundChannel.setReadable(false);

		// Start the connection attempt.
		ClientBootstrap cb = getRemoteBootstrap(ctx, e,
				new OutboundHandler(e.getChannel()));
		if (cb == null)
			throw new NullPointerException("ClientBootstrap");
		if (log.isDebugEnabled()) {
			log.debug("{}: start connect", proxyTitle());
		}
		ChannelFuture f = cb.connect();

		outboundChannel = f.getChannel();
		f.addListener(new ChannelFutureListener() {
			@Override
			public void operationComplete(ChannelFuture future)
					throws Exception {
				if (future.isSuccess()) {
					// Connection attempt succeeded:
					// Begin to accept incoming traffic.
					if (log.isDebugEnabled()) {
						log.debug("{}: connected", proxyTitle());
					}
					inboundChannel.setReadable(true);
				} else {
					// Close the connection if the connection attempt has
					// failed.
					if (log.isDebugEnabled()) {
						log.debug("{}: connect fail", proxyTitle());
					}
					inboundChannel.close();
				}
			}
		});
	}

	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
			throws Exception {
		proxyTo(outboundChannel, e.getMessage(), true);
	}

	protected void proxyTo(Channel channel, Object v, boolean isClient2remote)
			throws Exception {
		if (v instanceof ChannelBuffer) {
			ChannelBuffer msg = (ChannelBuffer) v;
			dumpMessage(msg, isClient2remote);
		}
		channel.write(v);
	}

	protected void dumpMessage(ChannelBuffer msg, boolean isClient2remote) {
		if (log.isDebugEnabled()) {
			log.debug("{}: {}({})", new Object[] { proxyTitle(),
					isClient2remote ? "client => remote" : "remote => client",
					msg.readableBytes() });
		}
	}

	@Override
	public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e)
			throws Exception {
		if (log.isDebugEnabled()) {
			log.debug("{}: client close", proxyTitle());
		}
		if (outboundChannel != null) {
			NettyUtil.closeOnFlush(outboundChannel);
		}
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
			throws Exception {
		NettyUtil.closeOnFlush(e.getChannel());
	}

	private class OutboundHandler extends SimpleChannelUpstreamHandler {

		private final Channel inboundChannel;

		OutboundHandler(Channel inboundChannel) {
			this.inboundChannel = inboundChannel;
		}

		@Override
		public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
				throws Exception {
			proxyTo(inboundChannel, e.getMessage(), false);
		}

		@Override
		public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e)
				throws Exception {
			if (log.isDebugEnabled()) {
				log.debug("{}: remote close", proxyTitle());
			}
			NettyUtil.closeOnFlush(inboundChannel);
		}

		@Override
		public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
				throws Exception {
			NettyUtil.closeOnFlush(e.getChannel());
		}
	}
}
